/*
 * Copyright (c) 2012, marco.tamburelli@gmail.com
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met: 
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * The views and conclusions contained in the software and documentation are those
 * of the authors and should not be interpreted as representing official policies, 
 * either expressed or implied, of the CometMI Project.
 */
package org.cometmi.gwt.server.util;

import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;

import javassist.bytecode.ClassFile;

import org.apache.log4j.Logger;

public class Discovery
{
	static protected Logger logger = Logger.getLogger(Discovery.class);

	private Map<String, List<String>> classMap;
	protected List<Class<?>> subTypes;

	private ExecutorService executor;
	private List<Scanner> scanners;

	private class ClassFileCallable implements Callable<String[]>
	{
		private InputStream is;

		ClassFileCallable(InputStream is)
		{
			this.is = is;
		}

		@Override
		public String[] call() throws Exception
		{
			ClassFile cf = new ClassFile(new DataInputStream(this.is));

			String[] interfaces = cf.getInterfaces();

			String[] descr = new String[interfaces.length + 2];
			descr[0] = cf.getName();
			descr[1] = cf.getSuperclass();

			for (int i = 0; i < interfaces.length; i++)
				descr[i + 2] = interfaces[i];

			this.is.close();

			return descr;
		}
	}

	private class JarScanner extends Scanner
	{
		protected JarScanner(File source)
		{
			super(source);
		}

		@Override
		protected void scan()
		{
			LinkedList<Future<String[]>> taskList = new LinkedList<Future<String[]>>();

			try
			{
				JarFile jar = new JarFile(super.source);

				Enumeration<JarEntry> entries = jar.entries();

				while (entries.hasMoreElements())
				{
					ZipEntry e = entries.nextElement();

					if (e.getName().toLowerCase().endsWith(".class"))
						taskList.add(Discovery.this.executor.submit(new ClassFileCallable(jar.getInputStream(e))));
				}

				jar.close();

				for (Future<String[]> task : taskList)
					try
					{
						updateClassMap(task.get());
					}
					catch (InterruptedException e1)
					{
						logger.error(
								"[Discovery.JarScanner] Impossible to complete the JAR scan because teh process has been interrupted.",
								e1);
					}
					catch (ExecutionException e1)
					{}
			}
			catch (FileNotFoundException e)
			{
				logger.error("[Discovery.JarScanner] File not found: " + JarScanner.super.source.getName(), e);
			}
			catch (IOException e)
			{
				logger.error("[Discovery.JarScanner] Unexpected IO exception: " + JarScanner.super.source.getName(), e);
			}
		}
	}

	private class DeepFileScanner extends Scanner
	{
		protected DeepFileScanner(File source)
		{
			super(source);
		}

		@Override
		protected void scan()
		{
			List<Future<String[]>> taskList = scan(super.source);

			for (Future<String[]> task : taskList)
				try
				{
					updateClassMap(task.get());
				}
				catch (InterruptedException e1)
				{
					logger.error(
							"[Discovery.JarScanner] Impossible to complete the JAR scan because teh process has been interrupted.",
							e1);
				}
				catch (ExecutionException e1)
				{}
		}

		private List<Future<String[]>> scan(File file)
		{
			LinkedList<Future<String[]>> taskList = new LinkedList<Future<String[]>>();

			try
			{
				File canonicalForm = file.getCanonicalFile();

				if (canonicalForm.isFile())
				{
					if (file.getName().toLowerCase().endsWith(".class"))
						taskList.add(Discovery.this.executor.submit(new ClassFileCallable(new FileInputStream(file))));
				}
				else if (canonicalForm.isDirectory()) for (File f : canonicalForm.listFiles())
					taskList.addAll(scan(f));
			}
			catch (FileNotFoundException e)
			{
				logger.error("[Discovery.DeepFileScanner] File not found: " + file.getName(), e);
			}
			catch (IOException e)
			{
				logger.error("[Discovery.DeepFileScanner] Unexpected IO exception: " + file.getName(), e);
			}

			return taskList;
		}
	}

	public Discovery()
	{
		this.classMap = new HashMap<String, List<String>>();

		this.scanners = new ArrayList<Scanner>(10);
		// TODO Not sure it will work properly....
		this.executor = Executors.newFixedThreadPool(100);
	}

	/**
	 * Adds a scanner to the list of available scanners.
	 * 
	 * @param scanner
	 */
	public void addScanner(Scanner scanner)
	{
		this.scanners.add(scanner);
	}

	/**
	 * Returns sub types of provided parent.
	 * 
	 * @param parentType The parent type.
	 * @return The list of fetched subtypes.
	 */
	public <T> List<Class<? extends T>> getSubTypesOf(Class<T> parentType)
	{
		/* In case it's still working */
		this.executor.shutdown();

		return new ClassProvider(this.classMap).getSubTypesOf(parentType);
	}

	/**
	 * Creates a deep file scanner for the provided path.
	 * 
	 * @param path A directory or a file.
	 * @return The requested scanner.
	 */
	public Scanner createDeepFileScanner(String path)
	{
		File source = new File(path);
		return new DeepFileScanner(source);
	}

	/**
	 * Creates a JAR scanner for the provided jar archive.
	 * 
	 * @param path The path to a JAR archive.
	 * @return The requested scanner.
	 */
	public Scanner createJarScanner(String path)
	{
		File source = new File(path);
		return new JarScanner(source);
	}

	/**
	 * Must be used within the scan process to update the class map when a class
	 * is found.
	 * 
	 * @param descr a descriptor consists of a an array whose first item is the
	 *        name of the class, second is the superclass, and others are
	 *        implemented interfaces.
	 */
	private void updateClassMap(String[] descr)
	{
		/* we define and update, list of sub-types for each element */
		for (int i = 1; i < descr.length; i++)
		{
			List<String> subclasses = this.classMap.get(descr[i]);

			if (subclasses == null)
			{
				subclasses = new ArrayList<String>();
				this.classMap.put(descr[i], subclasses);
			}

			subclasses.add(descr[0]);
		}
	}
}
